package la.iok.hzsvn.lewin.movie.service;

import com.fish.k.fileparser.factory.Convertor;
import com.fish.k.fileparser.factory.WordHtmlFactory;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import la.iok.hzsvn.lewin.movie.config.DfsProperties;
import la.iok.hzsvn.lewin.movie.core.RoleEnum;
import la.iok.hzsvn.lewin.movie.core.Token;
import la.iok.hzsvn.lewin.movie.core.exception.ErrorCode;
import la.iok.hzsvn.lewin.movie.core.model.DeviceVo;
import la.iok.hzsvn.lewin.movie.core.model.MovieVo;
import la.iok.hzsvn.lewin.movie.core.movieservice.param.MovieVersionListParam;
import la.iok.hzsvn.lewin.movie.entity.*;
import la.iok.hzsvn.lewin.movie.mapper.MovieInstanceMapper;
import la.iok.hzsvn.lewin.movie.model.MovieDeleteContext;
import la.iok.hzsvn.lewin.movie.model.PublishParam;
import la.iok.hzsvn.lewin.movie.services.MovieServiceInvoker;
import la.iok.hzsvn.lewin.mybatis.model.PageParam;
import la.iok.hzsvn.lewin.mybatis.model.PageVo;
import la.iok.hzsvn.lewin.mybatis.model.SortParam;
import la.iok.hzsvn.lewin.mybatis.service.BaseServiceImpl;
import la.iok.hzsvn.redis.model.ProgressKey;
import la.iok.hzsvn.redis.service.ProgressTaskService;
import la.iok.hzsvn.share.annotations.NotNull;
import la.iok.hzsvn.share.annotations.NotNullElement;
import la.iok.hzsvn.share.annotations.Nullable;
import la.iok.hzsvn.share.collection.IdOperators;
import la.iok.hzsvn.share.dfs.exception.DfsException;
import la.iok.hzsvn.share.dfs.model.DfsResult;
import la.iok.hzsvn.share.number.NumberUtils;
import la.iok.hzsvn.share.protocol.exception.ErrorCodeException;
import la.iok.hzsvn.share.tools.crypto.Sha1Util;
import la.iok.hzsvn.share.tools.db.DatabaseUtils;
import la.iok.hzsvn.share.utils.FileUtils;
import la.iok.hzsvn.share.utils.PathUtils;
import la.iok.hzsvn.share.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.*;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static la.iok.hzsvn.share.utils.FileUtils.getExtension;
import static la.iok.hzsvn.share.utils.FileUtils.makeFileName;

@Service
public class MovieInstanceServiceImpl
        extends BaseServiceImpl<MovieInstance, MovieInstanceMapper,Long>
            implements MovieInstanceService {
    private static final Logger logger = LoggerFactory.getLogger(MovieInstanceService.class);
    private final MovieService movieService;
    private final MovieCoverFileService movieCoverFileService;
    private final MovieAssistFileService movieAssistFileService;
    private final MovieAssistImageService movieAssistImageService;
    private final WebDfsService webDfsService;
    private final MovieSceneService movieSceneService;
    private final MovieSceneFileService movieSceneFileService;
    private final ClueCardGroupService clueCardGroupService;
    private final ClueCardService clueCardService;
    private final ClueCardFileService clueCardFileService;
    private final PlayRoleService playRoleService;
    private final PlayRoleFileService playRoleFileService;
    private final MovieSceneInstanceService movieSceneInstanceService;
    private final MovieEncryptCodeService movieEncryptCodeService;
    private final ClueCardGroupInstanceService clueCardGroupInstanceService;
    private final ClueCardInstanceService clueCardInstanceService;
    private final PlayRoleInstanceService playRoleInstanceService;
    private final DfsProperties dfsProperties;
    private final ProgressTaskService progressTaskService;
    private final MovieLevelService movieLevelService;
    private final BookChapterService bookChapterService;
    private final PlayRoleBookService playRoleBookService;
    private final PlayRoleBookFileService playRoleBookFileService;
    private final BookChapterInstanceService bookChapterInstanceService;
    private final PlayRoleBookInstanceService playRoleBookInstanceService;
    private final PlayRoleBookImageService playRoleBookImageService;
    private final DeviceLocalMovieService deviceLocalMovieService;
    private final MovieServiceInvoker movieServiceInvoker;
    private final InnerTestMovieService innerTestMovieService;
    private final MovieDownloadService movieDownloadService;

    public MovieInstanceServiceImpl(MovieInstanceMapper mapper,
                                    MovieService movieService,
                                    MovieCoverFileService movieCoverFileService,
                                    MovieAssistFileService movieAssistFileService,
                                    MovieAssistImageService movieAssistImageService,
                                    WebDfsService webDfsService,
                                    MovieSceneService movieSceneService,
                                    MovieSceneFileService movieSceneFileService,
                                    ClueCardGroupService clueCardGroupService,
                                    ClueCardService clueCardService,
                                    ClueCardFileService clueCardFileService,
                                    PlayRoleService playRoleService,
                                    PlayRoleFileService playRoleFileService,
                                    MovieSceneInstanceService movieSceneInstanceService,
                                    MovieEncryptCodeService movieEncryptCodeService,
                                    ClueCardGroupInstanceService clueCardGroupInstanceService,
                                    ClueCardInstanceService clueCardInstanceService,
                                    PlayRoleInstanceService playRoleInstanceService,
                                    DfsProperties dfsProperties,
                                    ProgressTaskService progressTaskService,
                                    MovieLevelService movieLevelService,
                                    BookChapterService bookChapterService,
                                    PlayRoleBookService playRoleBookService,
                                    PlayRoleBookFileService playRoleBookFileService,
                                    BookChapterInstanceService bookChapterInstanceService,
                                    PlayRoleBookInstanceService playRoleBookInstanceService,
                                    PlayRoleBookImageService playRoleBookImageService,
                                    DeviceLocalMovieService deviceLocalMovieService,
                                    MovieServiceInvoker movieServiceInvoker,
                                    InnerTestMovieService innerTestMovieService,
                                    MovieDownloadService movieDownloadService) {
        super(mapper);
        this.movieService = movieService;
        this.movieCoverFileService = movieCoverFileService;
        this.movieAssistFileService = movieAssistFileService;
        this.movieAssistImageService = movieAssistImageService;
        this.webDfsService = webDfsService;
        this.movieSceneService = movieSceneService;
        this.movieSceneFileService = movieSceneFileService;
        this.clueCardGroupService = clueCardGroupService;
        this.clueCardService = clueCardService;
        this.clueCardFileService = clueCardFileService;
        this.playRoleService = playRoleService;
        this.playRoleFileService = playRoleFileService;
        this.movieSceneInstanceService = movieSceneInstanceService;
        this.movieEncryptCodeService = movieEncryptCodeService;
        this.clueCardGroupInstanceService = clueCardGroupInstanceService;
        this.clueCardInstanceService = clueCardInstanceService;
        this.playRoleInstanceService = playRoleInstanceService;
        this.dfsProperties = dfsProperties;
        this.progressTaskService = progressTaskService;
        this.movieLevelService = movieLevelService;
        this.bookChapterService = bookChapterService;
        this.playRoleBookService = playRoleBookService;
        this.playRoleBookFileService = playRoleBookFileService;
        this.bookChapterInstanceService = bookChapterInstanceService;
        this.playRoleBookInstanceService = playRoleBookInstanceService;
        this.playRoleBookImageService = playRoleBookImageService;
        this.deviceLocalMovieService = deviceLocalMovieService;
        this.movieServiceInvoker = movieServiceInvoker;
        this.innerTestMovieService = innerTestMovieService;
        this.movieDownloadService = movieDownloadService;
    }
    private void updatePercent(@Nullable ProgressKey progressKey,int percent,String message){
        if(progressKey != null){
            progressTaskService.updatePercent(progressKey.getKey(),percent,message);
        }
    }

    @Transactional
    @Override
    public Long publish(@NotNull Token token, @Nullable Long id, @NotNull PublishParam param, @Nullable ProgressKey progressKey) {
        updatePercent(progressKey,1,"验证参数");
        validateParam(param);
        Movie movie = movieService.validate(token,id);
        updatePercent(progressKey,2,"检查剧本配置完整性");
        checkExist(id, param.getVersion());
        publishCheck(movie);
        MovieInstance movieInstance = new MovieInstance();
        BeanUtils.copyProperties(movie,movieInstance);
        movieInstance.setId(null);
        movieInstance.setMovieId(movie.getId());
        movieInstance.setPublishTime(new Date());
        movieInstance.setPlayTimes(0);
        movieInstance.setVersion(param.getVersion());
        movieCoverFileService.increment(movieInstance.getCoverFileId());
        movieAssistFileService.increment(movieInstance.getAssistFileId());
        insertSelective(movieInstance);
        publishMovieAssist(movieInstance,movieInstance.getAssistFileId());
        MovieEncryptCode encryptCode = movieEncryptCodeService.movieEncryptCode(id);
        if(encryptCode == null){
            encryptCode = movieEncryptCodeService.save(id);
        }
        String key = encryptCode.getKey();
        String keyId = encryptCode.getKeyId();
        //剧幕
        publishMovieScenes(id, movieInstance, key, keyId,progressKey);
        //线索
        updatePercent(progressKey,83,"正在处理线索");
        publishClueCards(id, movieInstance);
        updatePercent(progressKey,95,"正在处理角色");
        //角色
        List<PlayRole> playRoles = playRoleService.list(id);
        Map<Long,Long> roleIdMapRoleInstanceId = new HashMap<>();
        for(PlayRole role : playRoles){
            roleIdMapRoleInstanceId.put(role.getId(),playRoleInstanceService.save(role,movieInstance.getId()));
        }
        //电子书
        updatePercent(progressKey,97,"正在处理角色电子书");
        publishPlayRoleBooks(id,movieInstance.getId(),roleIdMapRoleInstanceId);
        movieService.update(id,"version",param.getVersion());
        updatePercent(progressKey,100,"完成");
        return movieInstance.getId();
    }

    /**
     * 验证对应版本的剧本是否已经存在.发布前做检查
     * @param id 剧本id
     * @param version 剧本版本
     */
    private void checkExist(@NotNull Long id, @NotNull String version) {
        MovieInstance mi = movieInst(id, version);
        ErrorCode.PROPERTY_CHECK_ERROR.assertNull(mi, ()->new Object[]{
                "版本",String.format("已经发布版本[%s],请不要重复发布相同版本",version)
        });
    }

    /**
     * 查询电子书包含的所有文件,包括doc文件,html文件,image文件
     */
    @NotNull
    @NotNullElement
    private List<PlayRoleBookFile> bookAllFiles(@NotNull @NotNullElement List<PlayRoleBook> books,@NotNull @NotNullElement List<PlayRoleBookInstance> nearestInstances,List<PlayRoleBookImage> images){
        Set<Long> fileIdSet = books.stream().map(PlayRoleBook::getFileId).filter(Objects::nonNull).collect(Collectors.toSet());
        fileIdSet.addAll(nearestInstances.stream().map(PlayRoleBookInstance::getFileId).filter(Objects::nonNull).collect(Collectors.toSet()));
        fileIdSet.addAll(nearestInstances.stream().map(PlayRoleBookInstance::getHtmlId).filter(Objects::nonNull).collect(Collectors.toSet()));
        fileIdSet.addAll(images.stream().map(PlayRoleBookImage::getFileId).filter(Objects::nonNull).collect(Collectors.toSet()));
        return playRoleBookFileService.listInProperty("id",fileIdSet);
    }

    /**
     * 发布剧本手册文件。需要把doc文件转换为html文件
     * @param movieInstance 发布的剧本实例
     * @param movieAssistFileId doc文档的id,MovieAssistFile的id
     */
    private void publishMovieAssist(@NotNull MovieInstance movieInstance,Long movieAssistFileId){
        if(!DatabaseUtils.isPoIdValid(movieAssistFileId)){
            return;
        }
        ErrorCode.PROPERTY_CHECK_ERROR.assertTrue(DatabaseUtils.isPoIdValid(movieInstance.getId()),"剧本实例","需要先保存剧本发布实例");
        boolean transform = true;
        // 查询使用这个文件的最初的那个发布实例（如果存在其他的实例,后续实例都是从那个实例复制来的)
        MovieInstance mi = executeSelect("assistFileId=" + movieAssistFileId + " and id != " + movieInstance.getId() + " order by id desc limit 1");
        if(mi != null){
            //如果存在,直接使用就可以了
            MovieAssistFile file = movieAssistFileService.select(mi.getAssistHtmlFileId());
            if(file != null && webDfsService.exist(webDfsService.dfsFile(file.getPath()))){
                boolean allExist = true;
                List<MovieAssistImage> theImg = movieAssistImageService.listByProperty("movieInstanceId",mi.getId());
                List<MovieAssistFile> files = movieAssistFileService.listInProperty("id",theImg.stream().map(MovieAssistImage::getFileId).collect(Collectors.toSet()));
                for(MovieAssistImage image : theImg){
                    MovieAssistFile img = IdOperators.find(files, image.getFileId());
                    if(img==null || !webDfsService.exist(webDfsService.dfsFile(img.getPath()))){
                        allExist = false;
                        break;
                    }
                }
                if(allExist){
                    update(movieInstance.getId(),"assistHtmlFileId",mi.getAssistHtmlFileId());
                    update(movieInstance.getId(), "assistHtml",mi.getAssistHtml());
                    movieAssistFileService.increment(mi.getAssistHtmlFileId());

                    theImg.forEach(image->{
                        movieAssistImageService.save(movieInstance.getId(),image.getFileId(),image.getFilePath());
                        movieAssistFileService.increment(image.getFileId());
                    });
                    transform = false;
                }
            }
        }
        if(transform){
            //下载doc文档到本地并转换成html
            Convertor convertor = new WordHtmlFactory();
            String path = prepareTempPath();
            MovieAssistFile file = movieAssistFileService.select(movieAssistFileId);
            String fileName = makeFileName(file.getDigest(),getExtension(file.getFileName()));
            File filePath = new File(PathUtils.join(path,fileName));
            try(FileOutputStream os = new FileOutputStream(filePath)){
                String docRelativePath = movieInstance.getAssist();
                webDfsService.downloadFile(os, webDfsService.dfsFile(file.getPath()));
                convertor.convert(PathUtils.join(path,fileName),PathUtils.join(path,file.getDigest()));
                File htmlFile = new File(PathUtils.join(path,file.getDigest() + ".html"));
                Long fileId = movieAssistFileService.uploadFile(htmlFile);
                update(movieInstance.getId(),"assistHtmlFileId",fileId);
                update(movieInstance.getId(), "assistHtml",FileUtils.replaceExtension(docRelativePath,"html"));
                movieAssistFileService.increment(fileId);

                //处理图片文件
                List<String> dirImages = PathUtils.listAllFiles(PathUtils.join(path,file.getDigest()),true);
                if(dirImages == null || dirImages.size() == 0){
                    return;
                }
                for(String img : dirImages){
                    File f = new File(img);
                    Long imgFileId = movieAssistFileService.uploadFile(f);
                    String storeParent = docRelativePath.substring(0,docRelativePath.length()-file.getFileName().length());
                    String imgPath = PathUtils.join(storeParent,PathUtils.relativePath(f.getAbsolutePath(),path));
                    movieAssistImageService.save(movieInstance.getId(),imgFileId,imgPath);
                    movieAssistFileService.increment(imgFileId);
                }
            }catch (IOException e){
                throw new DfsException("下载doc文件[" + file.getPath() + "]失败",e);
            } catch (Exception e) {
                throw new DfsException("转换doc文件[" + file.getPath() + "]失败",e);
            } finally {
                FileUtils.deleteFile(PathUtils.join(path,file.getDigest()),true);
                FileUtils.deleteFile(PathUtils.join(path,file.getDigest() + ".html"),true);
                if(filePath.exists()){
                    if(!filePath.delete()){
                        logger.error("删除临时文件["+filePath.getAbsolutePath()+"]失败");
                    }
                }
            }
        }
    }
    private void publishPlayRoleBooks(@NotNull Long movieId,@NotNull Long movieInstanceId,Map<Long,Long> roleIdMapRoleInstanceId) {
        List<BookChapter> chapters = bookChapterService.movieChapters(movieId);
        Map<Long,Long> chapterIdMapChapterInstanceId = new HashMap<>();
        for(BookChapter chapter : chapters){
            chapterIdMapChapterInstanceId.put(chapter.getId(),bookChapterInstanceService.save(chapter,movieInstanceId));
        }
        List<PlayRoleBook> books = playRoleBookService.movieRoleBooks(movieId);
        List<PlayRoleBookInstance> nearestInstance = playRoleBookInstanceService.nearest(books);
        List<PlayRoleBookImage> images = playRoleBookImageService.listInProperty("playRoleBookInstanceId",nearestInstance.stream().map(PlayRoleBookInstance::getId).collect(Collectors.toSet()));
        List<PlayRoleBookFile> files = bookAllFiles(books,nearestInstance,images);
        Convertor convertor = new WordHtmlFactory();
        for(PlayRoleBook book : books){
            boolean transform = true;
            PlayRoleBookInstance bookInstance = playRoleBookInstanceService.find(nearestInstance,book.getFileId());
            if(bookInstance != null){
                PlayRoleBookFile file = IdOperators.find(files, bookInstance.getHtmlId());
                if(file != null && webDfsService.exist(webDfsService.dfsFile(file.getPath()))){
                    boolean allExist = true;
                    List<PlayRoleBookImage> theImg = images.stream().filter(img->Objects.equals(img.getPlayRoleBookInstanceId(),bookInstance.getId())).collect(Collectors.toList());
                    for(PlayRoleBookImage image : theImg){
                        PlayRoleBookFile img = IdOperators.find(files, image.getFileId());
                        if(img==null || !webDfsService.exist(webDfsService.dfsFile(img.getPath()))){
                            allExist = false;
                            break;
                        }
                    }
                    if(allExist){
                        Long newBookInstanceId = playRoleBookInstanceService.save(book,roleIdMapRoleInstanceId.get(book.getPlayRoleId()),
                                chapterIdMapChapterInstanceId.get(book.getChapterId()),bookInstance.getHtmlId(),bookInstance.getHtmlPath());
                        playRoleBookFileService.increment(book.getFileId());
                        playRoleBookFileService.increment(bookInstance.getHtmlId());

                        theImg.forEach(image->{
                            playRoleBookImageService.save(newBookInstanceId,image.getFileId(),image.getFilePath());
                            playRoleBookFileService.increment(image.getFileId());
                        });
                        transform = false;
                    }
                }
            }
            if(transform){
                //下载doc文档到本地并转换成html
                String path = prepareTempPath();
                PlayRoleBookFile file = IdOperators.find(files,book.getFileId());
                String fileName = makeFileName(file.getDigest(),getExtension(file.getFileName()));
                File filePath = new File(PathUtils.join(path,fileName));
                try(FileOutputStream os = new FileOutputStream(filePath)){
                    webDfsService.downloadFile(os, webDfsService.dfsFile(file.getPath()));
                    convertor.convert(PathUtils.join(path,fileName),PathUtils.join(path,file.getDigest()));
                    File htmlFile = new File(PathUtils.join(path,file.getDigest() + ".html"));
                    String digest = Sha1Util.sha1(htmlFile);
                    DfsResult dfsResult = playRoleBookFileService.uploadFile(digest,htmlFile);
                    if(!dfsResult.success()){
                        throw new DfsException("上传html文件失败：" + dfsResult.getMessage());
                    }
                    Long fileId = playRoleBookFileService.save(file.getDigest() + ".html",dfsResult.getDescriptor().path(),digest);
                    Long newBookInstanceId = playRoleBookInstanceService.save(book, roleIdMapRoleInstanceId.get(book.getPlayRoleId()),
                            chapterIdMapChapterInstanceId.get(book.getChapterId()),fileId, FileUtils.replaceExtension(book.getFilePath(),"html"));
                    playRoleBookFileService.increment(fileId);
                    playRoleBookFileService.increment(book.getFileId());
                    //处理图片文件
                    List<String> dirImages = PathUtils.listAllFiles(PathUtils.join(path,file.getDigest()),true);
                    if(dirImages == null || dirImages.size() == 0){
                        continue;
                    }
                    for(String img : dirImages){
                        File f = new File(img);
                        digest = Sha1Util.sha1(f);
                        dfsResult = playRoleBookFileService.uploadFile(digest,f);
                        if(!dfsResult.success()){
                            throw new DfsException("上传图片文件失败：" + dfsResult.getMessage());
                        }
                        fileId = playRoleBookFileService.save(f.getName(),dfsResult.getDescriptor().path(),digest);
                        String storeParent = book.getFilePath().substring(0,book.getFilePath().length()-file.getFileName().length());
                        String imgPath = PathUtils.join(storeParent,PathUtils.relativePath(f.getAbsolutePath(),path));
                        playRoleBookImageService.save(newBookInstanceId,fileId,imgPath);
                        playRoleBookFileService.increment(fileId);
                    }
                }catch (IOException e){
                    throw new DfsException("下载doc文件[" + file.getPath() + "]失败",e);
                } catch (Exception e) {
                    throw new DfsException("转换doc文件[" + file.getPath() + "]失败",e);
                } finally {
                    FileUtils.deleteFile(PathUtils.join(path,file.getDigest()),true);
                    FileUtils.deleteFile(PathUtils.join(path,file.getDigest() + ".html"),true);
                    if(filePath.exists()){
                        if(!filePath.delete()){
                            logger.error("删除临时剧幕文件["+filePath.getAbsolutePath()+"]失败");
                        }
                    }
                }
            }
        }
    }

    private String prepareTempPath() {
        String path = StringUtils.isBlank(dfsProperties.getTmpPath()) ? PathUtils.getPath() : dfsProperties.getTmpPath();
        File pathFile = new File(path);
        if(!pathFile.exists()){
            if(!pathFile.mkdirs()){
                throw new DfsException("创建目录["+path+"]失败");
            }
        }
        return path;
    }

    @Transactional
    @Override
    public Long publish(Token token, Long id, PublishParam param) {
        return publish(token,id,param,null);
    }

    @Override
    public PageVo<MovieVo> page(int page, int limit,@Nullable String orderColumn, @Nullable String order, @Nullable String name,
                                @Nullable String author, @Nullable String tag, @Nullable Integer minPlayers, @Nullable Integer maxPlayers,
                                @Nullable Integer minDuration, @Nullable Integer maxDuration, @Nullable Long movieLevelId,
                                @Nullable Integer status, Integer excludeLocal,String deviceCode,Long[] innerTestIds) {
        orderColumn = StringUtils.isBlank(orderColumn)?"publishTime":orderColumn;
        order = "ascending".equalsIgnoreCase(order)?"ASC":"DESC";
        Long[] excludes = null;
        if(excludeLocal != null && excludeLocal == 1){
            excludes = deviceLocalMovieService.loadMovieList(deviceCode);
        }
        Page<MovieInstance> list = PageHelper.startPage(page,limit);
        mapper.list(orderColumn,order,name,author,tag,minPlayers,maxPlayers,minDuration,maxDuration,movieLevelId,status,excludes,innerTestIds);
        Set<Long> movieLevelIds = list.stream().map(MovieInstance::getMovieLevelId).collect(Collectors.toSet());
        List<MovieLevel> movieLevels = movieLevelService.listInProperty("id",movieLevelIds);
        return page(list,mi->po2vo(mi,IdOperators.find(movieLevels,mi.getMovieLevelId())));
    }

    @Override
    public PageVo<MovieVo> page(Token token, int page, int limit, String orderColumn, String order, String name,
                                String author, String tag, Integer minPlayers, Integer maxPlayers, Integer minDuration,
                                Integer maxDuration, Long movieLevelId, Integer excludeLocal, String deviceCode) {
        if(token != null){
            if(token.hasAnyAuthority(RoleEnum.Admin)){
                return page(page,limit,orderColumn,order,name,author,tag,
                        minPlayers,maxPlayers,minDuration,maxDuration,movieLevelId,null,excludeLocal,deviceCode,null);
            }else{
                DeviceVo device = movieServiceInvoker.deviceInfo(deviceCode);
                if(device == null){
                    device = movieServiceInvoker.deviceInfo(token.getDeviceId());
                }
                if(device != null){
                    Long[] innerTestIds = innerTestMovieService.listByProperty("roomId",device.getRoomId())
                            .stream().map(InnerTestMovie::getMovieId).distinct().toArray(Long[]::new);
                    return page(page,limit,orderColumn,order,name,author,tag,
                            minPlayers,maxPlayers,minDuration,maxDuration,movieLevelId,Movie.STATUS_ONLINE,excludeLocal,deviceCode,innerTestIds);
                }
            }
        }
        return page(page,limit,orderColumn,order,name,author,tag,minPlayers,
                maxPlayers,minDuration,maxDuration,movieLevelId,Movie.STATUS_ONLINE,excludeLocal,deviceCode,null);
    }

    @Override
    public PageVo<MovieVo> page(int page, int limit,@Nullable String orderColumn, @Nullable String order, @Nullable String name,
                                @Nullable String author, @Nullable String tag, @Nullable Integer minPlayers, @Nullable Integer maxPlayers,
                                @Nullable Integer minDuration, @Nullable Integer maxDuration, @Nullable Long movieLevelId,
                                @Nullable Integer status) {
        return page(page,limit,orderColumn,order,name,author,tag,minPlayers,maxPlayers,minDuration,maxDuration,movieLevelId,status,0,null,null);
    }

    @Override
    public PageVo<MovieVo> versions(Token token, PageParam pageParam, SortParam sortParam, MovieVersionListParam listParam) {
        token.validAnyAuthority(RoleEnum.Admin);
        String orderColumn = sortParam.orderColumn("publishTime");
        String order = sortParam.order("DESC");
        Page<MovieInstance> list = PageHelper.startPage(pageParam.getPage(),pageParam.getLimit());
        mapper.listByParam(order, orderColumn, listParam);
        Set<Long> movieLevelIds = list.stream().map(MovieInstance::getMovieLevelId).collect(Collectors.toSet());
        List<MovieLevel> movieLevels = movieLevelService.listInProperty("id",movieLevelIds);
        return page(list, mi->po2vo(mi,IdOperators.find(movieLevels,mi.getMovieLevelId())));
    }

    @Override
    public List<MovieVo> latestMovies(Long[] movieIds) {
        if(movieIds == null || movieIds.length == 0){
            return Collections.emptyList();
        }
        movieIds = Stream.of(movieIds).filter(Objects::nonNull).toArray(Long[]::new);
        if(movieIds.length == 0){
            return Collections.emptyList();
        }
        List<MovieInstance> list = mapper.mostRecentList(movieIds);
        return po2vo(list);
    }

    @NotNull
    @NotNullElement
    private List<MovieVo> po2vo(@NotNull List<MovieInstance> list) {
        Set<Long> movieLevelIds = list.stream().map(MovieInstance::getMovieLevelId).collect(Collectors.toSet());
        List<MovieLevel> movieLevels = movieLevelService.listInProperty("id",movieLevelIds);
        return list.stream().map(mi->po2vo(mi, IdOperators.find(movieLevels, mi.getMovieLevelId()))).collect(Collectors.toList());
    }

    @NotNull
    @NotNullElement
    @Override
    public List<MovieVo> publishes(@NotNull Token token, @Nullable Long movieId) {
        token.validAnyAuthority(RoleEnum.Admin);
        if(movieId == null){
            return Collections.emptyList();
        }
        List<MovieInstance> list = mapper.listPublishes(movieId);
        return po2vo(list);
    }

    @Nullable
    @Override
    public MovieInstance mostRecent(@Nullable Long movieId) {
        if(movieId == null){
            return null;
        }
        return mapper.mostRecent(movieId);
    }

    @NotNull
    @Override
    public MovieVo po2vo(@NotNull MovieInstance movie) {
        MovieLevel level = movieLevelService.select(movie.getMovieLevelId());
        return po2vo(movie,level);
    }

    private MovieVo po2vo(@NotNull MovieInstance movie, MovieLevel level){
        MovieVo vo = new MovieVo();
        BeanUtils.copyProperties(movie,vo);
        if(level != null){
            vo.setPrice(level.getPrice());
            vo.setOpenPrice(level.getOpenPrice());
            vo.setMovieLevelName(level.getName());
        }
        vo.setTag(showTag(movie.getTag()));
        vo.setId(movie.getMovieId());
        vo.setMovieInstanceId(movie.getId());
        return vo;
    }

    @Transactional
    @Override
    public void online(@NotNull Token token, @NotNull Long movieId) {
        ErrorCode.ADMIN_DENIED.assertTrue(token.hasAuthority(RoleEnum.Admin));
        mapper.online(movieId);
        movieService.update(movieId,"status",Movie.STATUS_ONLINE);
    }

    @Transactional
    @Override
    public void offline(Token token, Long movieId) {
        ErrorCode.ADMIN_DENIED.assertTrue(token.hasAuthority(RoleEnum.Admin));
        mapper.offline(movieId);
        movieService.update(movieId,"status",Movie.STATUS_OFFLINE);
    }

    @Transactional
    @Override
    public void updateLevel(@NotNull Token token, @NotNull Long movieId, @NotNull Long movieLevelId) {
        ErrorCode.ADMIN_DENIED.assertTrue(token.hasAuthority(RoleEnum.Admin));
        MovieLevel level = movieLevelService.select(movieLevelId);
        ErrorCode.MOVIE_LEVEL_ERROR.assertNotNull(level,movieLevelId);
        MovieInstance mi = mapper.mostRecent(movieId);
        if(mi != null){
            update(mi.getId(),"movieLevelId",level.getId());
        }
        movieService.update(movieId,"movieLevelId",level.getId());
    }

    @Override
    public int count(@Nullable Integer status) {
        return mapper.count(status);
    }

    @Override
    public MovieInstance movieInst(@Nullable Long movieId, @Nullable String version) {
        if(movieId == null || StringUtils.isBlank(version)){
            return null;
        }
        return mapper.movieInst(movieId,version);
    }

    @Override
    public MovieInstance movieInst2(@Nullable Long movieId, @Nullable String version) {
        MovieInstance mi = movieInst(movieId,version);
        ErrorCode.MOVIE_INSTANCE_NOT_EXIST.assertNotNull(mi,()->{
            Movie m = movieService.select(movieId);
            return m == null?new Object[]{movieId+"(" + version + ")"}:new Object[]{m.getName()+"(" + version + ")"};
        });
//        ErrorCode.MOVIE_INSTANCE_NOT_EXIST.assertEqual(Movie.STATUS_ONLINE,mi.getStatus(),()->{
//            Movie m = movieService.select(movieId);
//            return m == null?new Object[]{movieId+"(" + version + ")"}:new Object[]{m.getName()+"(" + version + ")"};
//        });
        return mi;
    }

    @Transactional
    @Override
    public void incrementPlayTimes(@Nullable Long movieId, @Nullable String version) {
        MovieInstance mi = movieInst(movieId,version);
        ErrorCode.RECORD_NOT_EXIST.assertNotNull(mi, ()->new Object[]{
                String.format("剧本id:%s-版本:%s",movieId,version)
        });
        increment("playTimes",mi.getId());
        movieService.increment("playTimes",movieId);
    }

    @Transactional
    @Override
    public void delete(@NotNull Token token,Long movieId, String version) {
        //movieService.validate(token,movieId);
        token.validAnyAuthority(RoleEnum.Admin);
        MovieInstance mi = movieInst2(movieId, version);
        doDeleteMovieInstance(mi);
    }

    @Transactional
    @Override
    public void delete(Token token, Long id) {
        token.validAnyAuthority(RoleEnum.Admin);
        MovieInstance mi = select(id);
        ErrorCode.RECORD_NOT_EXIST.assertNotNull(mi, id);
        doDeleteMovieInstance(mi);
    }

    private void doDeleteMovieInstance(@NotNull MovieInstance mi) {
        ErrorCode.PROPERTY_CHECK_ERROR.assertEqual(1, mi.getFileDeleted(),"文件","请先删除剧本文件之后再删除剧本");
        Integer times = mi.getPlayTimes();
        ErrorCode.MOVIE_PLAYS_DELETE_ERROR.assertEqual(0,times,()->new Object[]{
                String.format("id:%s-版本:%s", mi.getMovieId(), mi.getVersion()),String.valueOf(times)
        });
        //剧本手册文件
        List<MovieAssistImage> assistImages = movieAssistImageService.assistImages(mi.getId());
        assistImages.forEach(img->ErrorCode.PROPERTY_CHECK_ERROR.assertEqual(1, img.getFileDeleted(),()->new Object[]{
                "剧本手册文件图片",String.format("剧本手册图片[%s]的文件未标记为已删除",img.getId())
        }));
        movieAssistImageService.deleteInProperty("id",assistImages.stream().map(MovieAssistImage::getId).collect(Collectors.toSet()));
        //删除剧幕，需要先标记剧幕的文件已删除
        List<MovieSceneInstance> movieSceneInstances = movieSceneInstanceService.listByProperty("movieInstanceId", mi.getId(),"sequence");
        movieSceneInstances.forEach(msi->{
            ErrorCode.PROPERTY_CHECK_ERROR.assertEqual(1,msi.getFileDeleted(),()->new Object[]{
                    "剧幕",String.format("剧幕[%s]的文件未标记为已删除",msi.getId())
            });
        });
        movieSceneInstanceService.deleteInProperty("id",movieSceneInstances.stream().map(MovieSceneInstance::getId).collect(Collectors.toSet()));
        //线索分组
        List<ClueCardGroupInstance> groups = clueCardGroupInstanceService.listByProperty("movieInstanceId",mi.getId());
        //线索
        List<ClueCardInstance> clueCards = clueCardInstanceService.listInProperty("groupInstanceId",groups.stream().map(ClueCardGroupInstance::getId).collect(Collectors.toSet()));
        clueCards.forEach(cci->{
            ErrorCode.PROPERTY_CHECK_ERROR.assertEqual(1,cci.getFileDeleted(),()->new Object[]{
                    "线索",String.format("线索[%s]的文件未标记为已删除",cci.getId())
            });
        });
        clueCardInstanceService.deleteInProperty("id",clueCards.stream().map(ClueCardInstance::getId).collect(Collectors.toSet()));
        clueCardGroupInstanceService.deleteInProperty("id",groups.stream().map(ClueCardGroupInstance::getId).collect(Collectors.toSet()));
        //角色
        List<PlayRoleInstance> roles = playRoleInstanceService.listByProperty("movieInstanceId",mi.getId());
        roles.forEach(role->ErrorCode.PROPERTY_CHECK_ERROR.assertEqual(1,role.getFileDeleted(),()->new Object[]{
                "角色",String.format("角色[%s]的文件未标记为已删除",role.getId())
        }));
        playRoleInstanceService.deleteInProperty("id",roles.stream().map(PlayRoleInstance::getId).collect(Collectors.toSet()));
        //章节
        List<BookChapterInstance> chapters = bookChapterInstanceService.listByProperty("movieInstanceId",mi.getId());
        List<PlayRoleBookInstance> books = playRoleBookInstanceService.listInProperty("chapterInstanceId",chapters.stream().map(BookChapterInstance::getId).collect(Collectors.toSet()));
        books.forEach(book->ErrorCode.PROPERTY_CHECK_ERROR.assertEqual(1,book.getFileDeleted(),()->new Object[]{
                "电子书",String.format("电子书[%s]的文件未标记为已删除",book.getId())
        }));
        playRoleBookInstanceService.deleteInProperty("id",books.stream().map(PlayRoleBookInstance::getId).collect(Collectors.toSet()));
        bookChapterInstanceService.deleteInProperty("id",chapters.stream().map(BookChapterInstance::getId).collect(Collectors.toSet()));
        //电子书图片
        List<PlayRoleBookImage> images = playRoleBookImageService.listInProperty("playRoleBookInstanceId",books.stream().map(PlayRoleBookInstance::getId).collect(Collectors.toSet()));
        images.forEach(img->ErrorCode.PROPERTY_CHECK_ERROR.assertEqual(1, img.getFileDeleted(),()->new Object[]{
                "电子书图片",String.format("电子书图片[%s]的文件未标记为已删除",img.getId())
        }));
        playRoleBookImageService.deleteInProperty("id",images.stream().map(PlayRoleBookImage::getId).collect(Collectors.toSet()));
        // 删除实例
        ErrorCode.RECORD_MODIFIED_ERROR.assertEqual(1, mapper.deleteInstance(mi.getId()), mi.getId());
        MovieInstance mi2 = mostRecent(mi.getMovieId());
        String v = mi2 != null? mi2.getVersion():"";
        movieService.update(mi.getMovieId(),"version",v);
    }

    @Transactional
    @Override
    public MovieDeleteContext deleteMovieFiles(Token token, Long movieId, String version) {
        //movieService.validate(token,movieId);
        token.validAnyAuthority(RoleEnum.Admin);
        MovieInstance mi = movieInst2(movieId,version);
        return doDeleteMovieFiles(mi);
    }

    private MovieDeleteContext doDeleteMovieFiles(@NotNull MovieInstance mi) {
        // 标记实例文件已删除
        ErrorCode.RECORD_MODIFIED_ERROR.assertEqual(1, markFileDeleted(mi.getId()),mi.getId());
        MovieDeleteContext ctx = new MovieDeleteContext();
        //封面文件
        ctx.setCoverFileId(mi.getCoverFileId());
        ctx.setMovieAssistFileIds(new HashSet<>());
        ctx.getMovieAssistFileIds().add(mi.getAssistFileId());
        ctx.getMovieAssistFileIds().add(mi.getAssistHtmlFileId());
        Map<Long,Integer> movieSceneFileCountMap = new HashMap<>();
        Map<Long,Integer> clueCardFileCountMap = new HashMap<>();
        movieCoverFileService.decrement(mi.getCoverFileId());
        movieAssistFileService.decrement(mi.getAssistFileId());
        movieAssistFileService.decrement(mi.getAssistHtmlFileId());
        List<MovieAssistImage> assistImages = movieAssistImageService.assistImages(mi.getId());
        assistImages.forEach(image->{
            movieAssistFileService.decrement(image.getFileId());
            ctx.getMovieAssistFileIds().add(image.getFileId());
        });
        movieAssistImageService.markFileDeleted(assistImages.stream().map(MovieAssistImage::getId).collect(Collectors.toSet()));
        //减少剧幕文件引用,包括源文件和加密文件
        List<MovieSceneInstance> movieSceneInstances = movieSceneInstanceService.listByProperty("movieInstanceId", mi.getId(),"sequence");
        movieSceneInstances.forEach(msi->{
            movieSceneFileCountMap.merge(msi.getFileId(), 1, Integer::sum);
            movieSceneFileCountMap.merge(msi.getEncodedFileId(),1,Integer::sum);
        });
        movieSceneFileCountMap.forEach((fileId,cnt)->movieSceneFileService.decrement("count",fileId,cnt));
        ctx.setMovieSceneFileIds(movieSceneFileCountMap.keySet());
        movieSceneInstanceService.markFileDeleted(movieSceneInstances.stream().map(MovieSceneInstance::getId).collect(Collectors.toSet()));
        //线索分组
        List<ClueCardGroupInstance> groups = clueCardGroupInstanceService.listByProperty("movieInstanceId",mi.getId());
        //线索
        List<ClueCardInstance> clueCards = clueCardInstanceService.listInProperty("groupInstanceId",groups.stream().map(ClueCardGroupInstance::getId).collect(Collectors.toSet()));
        clueCards.forEach(cci->{
            clueCardFileCountMap.merge(cci.getHdFileId(),1,Integer::sum);
            clueCardFileCountMap.merge(cci.getSdFileId(),1,Integer::sum);
        });
        clueCardFileCountMap.forEach((fileId,cnt)->clueCardFileService.decrement("count",fileId,cnt));
        ctx.setClueCardFilIds(clueCardFileCountMap.keySet());
        clueCardInstanceService.markFileDeleted(clueCards.stream().map(ClueCardInstance::getId).collect(Collectors.toSet()));
        clueCardGroupInstanceService.markFileDeleted(groups.stream().map(ClueCardGroupInstance::getId).collect(Collectors.toSet()));
        //角色
        List<PlayRoleInstance> roles = playRoleInstanceService.listByProperty("movieInstanceId",mi.getId());
        roles.forEach(role->playRoleFileService.decrement(role.getFileId()));
        ctx.setPlayRoleFileIds(roles.stream().map(PlayRoleInstance::getFileId).collect(Collectors.toSet()));
        playRoleInstanceService.markFileDeleted(roles.stream().map(PlayRoleInstance::getId).collect(Collectors.toSet()));
        //章节
        List<BookChapterInstance> chapters = bookChapterInstanceService.listByProperty("movieInstanceId",mi.getId());
        List<PlayRoleBookInstance> books = playRoleBookInstanceService.listInProperty("chapterInstanceId",chapters.stream().map(BookChapterInstance::getId).collect(Collectors.toSet()));
        ctx.setPlayRoleBookFileIds(new HashSet<>());
        books.forEach(book->{
            playRoleBookFileService.decrement(book.getFileId());
            playRoleBookFileService.decrement(book.getHtmlId());
            ctx.getPlayRoleBookFileIds().add(book.getFileId());
            ctx.getPlayRoleBookFileIds().add(book.getHtmlId());
        });
        playRoleBookInstanceService.markFileDeleted(books.stream().map(PlayRoleBookInstance::getId).collect(Collectors.toSet()));
        bookChapterInstanceService.markFileDeleted(chapters.stream().map(BookChapterInstance::getId).collect(Collectors.toSet()));
        //电子书图片
        List<PlayRoleBookImage> images = playRoleBookImageService.listInProperty("playRoleBookInstanceId",books.stream().map(PlayRoleBookInstance::getId).collect(Collectors.toSet()));
        images.forEach(img->{
            playRoleBookFileService.decrement(img.getFileId());
            ctx.getPlayRoleBookFileIds().add(img.getFileId());
        });
        playRoleBookImageService.markFileDeleted(images.stream().map(PlayRoleBookImage::getId).collect(Collectors.toSet()));
        return ctx;
    }

    @Override
    public MovieDeleteContext deleteMovieFiles(Token token, Long movieInstanceId) {
        token.validAnyAuthority(RoleEnum.Admin);
        MovieInstance mi = select(movieInstanceId);
        ErrorCode.RECORD_NOT_EXIST.assertNotNull(mi, movieInstanceId);
        return doDeleteMovieFiles(mi);
    }

    /**
     * 标记剧本文件已经删除
     */
    private int markFileDeleted(Long movieInstanceId){
        return mapper.markFileDeleted(movieInstanceId);
    }
    @Override
    public void cleanMovieFile(MovieDeleteContext ctx) {
        if(ctx == null){
            return;
        }
        //偿试删除封面文件
        doCleanFile(movieCoverFileService,ctx.getCoverFileId());
        //剧本手册文件
        if(ctx.getMovieAssistFileIds()!=null){
            ctx.getMovieAssistFileIds().forEach(id->doCleanFile(movieAssistFileService, id));
        }
        //偿试删除剧幕文件
        if(ctx.getMovieSceneFileIds() != null){
            ctx.getMovieSceneFileIds().forEach(id->doCleanFile(movieSceneFileService,id));
        }
        //偿试删除线索文件
        if(ctx.getClueCardFilIds() != null){
            ctx.getClueCardFilIds().forEach(id->doCleanFile(clueCardFileService,id));
        }
        // 偿试删除角色文件
        if(ctx.getPlayRoleFileIds() != null){
            ctx.getPlayRoleFileIds().forEach(id->doCleanFile(playRoleFileService,id));
        }
        // 偿试删除角色电子书文件
        if(ctx.getPlayRoleBookFileIds() != null){
            ctx.getPlayRoleBookFileIds().forEach(id->doCleanFile(playRoleBookFileService,id));
        }
    }

    @Transactional
    @Override
    public MovieDeleteContext deleteMovie(Token token, Long id) {
        Movie movie = movieService.validate(token,id);
        Integer times = movie.getPlayTimes();
        ErrorCode.MOVIE_PLAYS_DELETE_ERROR.assertEqual(0,times,()->new Object[]{
                String.format("id:%s-版本:%s",id,movie.getVersion()),String.valueOf(times)
        });
        int count = count("movieId",id);
        ErrorCode.MOVIE_INSTANCE_EXIST_DELETE_ERROR.assertEqual(0,count,movie.getName(),count);
        MovieDeleteContext ctx = new MovieDeleteContext();
        //封面文件
        ctx.setCoverFileId(movie.getCoverFileId());
        movieCoverFileService.decrement(movie.getCoverFileId());
        // 剧本手册
        ctx.setMovieAssistFileIds(new HashSet<>());
        ctx.getMovieAssistFileIds().add(movie.getAssistFileId());
        movieAssistFileService.decrement(movie.getAssistFileId());
        //减少剧幕文件引用,包括源文件和加密文件
        Map<Long,Integer> movieSceneFileCountMap = new HashMap<>();
        List<MovieScene> movieScenes = movieSceneService.listByProperty("movieId", id,"sequence");
        movieScenes.forEach(movieScene->{
            movieSceneFileCountMap.merge(movieScene.getFileId(), 1, Integer::sum);
        });
        movieSceneFileCountMap.forEach((fileId,cnt)->movieSceneFileService.decrement("count",fileId,cnt));
        ctx.setMovieSceneFileIds(movieSceneFileCountMap.keySet());
        movieSceneService.deleteInProperty("id",movieScenes.stream().map(MovieScene::getId).collect(Collectors.toSet()));
        //线索分组
        List<ClueCardGroup> groups = clueCardGroupService.listByProperty("movieId",id);
        //线索
        Map<Long,Integer> clueCardFileCountMap = new HashMap<>();
        List<ClueCard> clueCards = clueCardService.listInProperty("groupId",groups.stream().map(ClueCardGroup::getId).collect(Collectors.toSet()));
        clueCards.forEach(clueCard->{
            clueCardFileCountMap.merge(clueCard.getHdFileId(),1,Integer::sum);
            clueCardFileCountMap.merge(clueCard.getSdFileId(),1,Integer::sum);
        });
        clueCardFileCountMap.forEach((fileId,cnt)->clueCardFileService.decrement("count",fileId,cnt));
        ctx.setClueCardFilIds(clueCardFileCountMap.keySet());
        clueCardService.deleteInProperty("id",clueCards.stream().map(ClueCard::getId).collect(Collectors.toSet()));
        clueCardGroupService.deleteInProperty("id",groups.stream().map(ClueCardGroup::getId).collect(Collectors.toSet()));
        //角色
        List<PlayRole> roles = playRoleService.listByProperty("movieId",id);
        roles.forEach(role->playRoleFileService.decrement(role.getFileId()));
        ctx.setPlayRoleFileIds(roles.stream().map(PlayRole::getFileId).collect(Collectors.toSet()));
        playRoleService.deleteInProperty("id",roles.stream().map(PlayRole::getId).collect(Collectors.toSet()));
        //章节
        List<BookChapter> chapters = bookChapterService.listByProperty("movieId",id);
        List<PlayRoleBook> books = playRoleBookService.listInProperty("chapterId",chapters.stream().map(BookChapter::getId).collect(Collectors.toSet()));
        ctx.setPlayRoleBookFileIds(new HashSet<>());
        books.forEach(book->{
            playRoleBookFileService.decrement(book.getFileId());
            ctx.getPlayRoleBookFileIds().add(book.getFileId());
        });
        playRoleBookService.deleteInProperty("id",books.stream().map(PlayRoleBook::getId).collect(Collectors.toSet()));
        bookChapterService.deleteInProperty("id",chapters.stream().map(BookChapter::getId).collect(Collectors.toSet()));
        // 删除剧本
        movieService.delete(id);
        return ctx;
    }

    @Override
    public MovieInstance movieDetail(Token token, Long id, String deviceCode) {
        MovieInstance movieInstance = mostRecent(id);
        ErrorCode.MOVIE_NOT_EXIST.assertNotNull(movieInstance,id);
        validateStatusIfNeed(token, id, deviceCode, movieInstance);
        movieDownloadService.saveDownloadMovie(token,id,movieInstance.getId());
        return movieInstance;
    }

    @Override
    public MovieInstance movieDetail(Token token, Long movieId, String version, String deviceCode) {
        MovieInstance mi = movieInst(movieId,version);
        ErrorCode.MOVIE_INSTANCE_NOT_EXIST.assertNotNull(mi,()->{
            Movie m = movieService.select(movieId);
            return m == null?new Object[]{movieId+"(" + version + ")"}:new Object[]{m.getName()+"(" + version + ")"};
        });
        validateStatusIfNeed(token,movieId,deviceCode,mi);
        return mi;
    }

    private void validateStatusIfNeed(Token token, Long id, String deviceCode, MovieInstance movieInstance) {
        DeviceVo device = movieServiceInvoker.deviceInfo(deviceCode);
        if(device == null){
            device = movieServiceInvoker.deviceInfo(token.getDeviceId());
        }
        boolean validateStatus = true;
        if(device != null){
            List<Long> innerTestIds = innerTestMovieService.listByProperty("roomId",device.getRoomId())
                    .stream().map(InnerTestMovie::getMovieId).distinct().collect(Collectors.toList());
            if(innerTestIds.contains(id)){
                validateStatus = false;
            }
        }
        if(validateStatus){
          //  ErrorCode.MOVIE_NOT_EXIST.assertEqual(Movie.STATUS_ONLINE, movieInstance.getStatus(), id);
        }
    }

    private <T extends MovieFile> void doCleanFile(MovieFileService<T> service, Long id){
        try{
            if(id == null){
                return;
            }
            service.deleteFreeFileWithoutAuthorityCheck(id);
        }catch (ErrorCodeException e){
            logger.info("文件不能删除",e);
        }catch (Exception e){
            logger.error("删除文件异常",e);
        }
    }

    private String[] showTag(String tag){
        return org.springframework.util.StringUtils.tokenizeToStringArray(tag,";");
    }

    private void publishClueCards(Long id, MovieInstance movieInstance) {
        List<ClueCardGroup> groups = clueCardGroupService.clueCardGroups(id);
        List<ClueCard> clueCards = clueCardService.clueCards(groups.stream().map(ClueCardGroup::getId).collect(Collectors.toSet()));
        for(ClueCardGroup group : groups){
            Long groupInstanceId = clueCardGroupInstanceService.save(group, movieInstance.getId());
            List<ClueCard> list = clueCards.stream().filter(item-> Objects.equals(item.getGroupId(),group.getId())).collect(Collectors.toList());
            for(ClueCard clueCard : list){
                clueCardInstanceService.save(clueCard,groupInstanceId);
            }
        }
    }

    private void publishMovieScenes(Long id, MovieInstance movieInstance, String key, String keyId,@Nullable ProgressKey progressKey) {
        List<MovieScene> scenes = movieSceneService.movieScenes(id);
        List<MovieSceneFile> sceneFiles = movieSceneFileService.listInProperty("id",scenes.stream().map(MovieScene::getFileId).collect(Collectors.toSet()));
        int step = 70 / scenes.size();
        int start = 10;  //起始进度
        for(MovieScene movieScene : scenes) {
            updatePercent(progressKey,start,"正在处理剧幕:" + movieScene.getName());
            start+= step;
            boolean encode = true;
            MovieSceneInstance msi = movieSceneInstanceService.select(id,movieScene.getFileId());
            if(msi != null){
                MovieSceneFile file = movieSceneFileService.select(msi.getEncodedFileId());
                if(file != null && webDfsService.exist(webDfsService.dfsFile(file.getPath()))){
                    //已经有一个现成的了,就不用再去加密了
                    encode = false;
                    movieSceneInstanceService.save(movieScene, movieInstance.getId(),msi.getEncodedFileId());
                    movieSceneFileService.increment(msi.getEncodedFileId());
                    movieSceneFileService.increment(movieScene.getFileId());
                }
            }
            if(encode){
                //下载文件到本地并加密然后再上传
                MovieSceneFile file = IdOperators.find(sceneFiles,movieScene.getFileId());
                String path = prepareTempPath();
                String fileName = makeFileName(file.getDigest(),getExtension(file.getFileName()));
                File filePath = new File(PathUtils.join(path,fileName));
                File encodedFile = new File(PathUtils.join(path,"encode",fileName));
                if(encodedFile.exists()){
                    if(!encodedFile.delete()){
                        throw new DfsException("临时加密剧幕文件["+encodedFile.getAbsolutePath()+"]已经存在并且删除失败");
                    }
                }else{
                    File encodePath = encodedFile.getParentFile();
                    if(!encodePath.exists()){
                        if(!encodePath.mkdirs()){
                            throw new DfsException("创建加密剧幕文件存储路径["+encodePath.getAbsolutePath()+"]失败");
                        }
                    }
                }
                try(FileOutputStream os = new FileOutputStream(filePath)){
                    webDfsService.downloadFile(os, webDfsService.dfsFile(file.getPath()));
                    if(runExecute("ffmpeg -i " + filePath.getAbsolutePath() + " -y -c:v copy -c:a copy -encryption_scheme cenc-aes-ctr -encryption_key " + key + " -encryption_kid " + keyId + " " + encodedFile.getAbsolutePath())){
                        String digest = Sha1Util.sha1(encodedFile);
                        DfsResult dfsResult = movieSceneFileService.uploadFile(digest,encodedFile);
                        if(!dfsResult.success()){
                            throw new DfsException("上传剧幕加密文件失败：" + dfsResult.getMessage());
                        }
                        Long fileId = movieSceneFileService.save(file.getFileName(),dfsResult.getDescriptor().path(),digest);
                        movieSceneInstanceService.save(movieScene, movieInstance.getId(),fileId);
                        movieSceneFileService.increment(fileId);
                        movieSceneFileService.increment(movieScene.getFileId());
                    }else{
                        throw new DfsException("加密剧幕[" + movieScene.getName() + "]失败");
                    }
                }catch (IOException e){
                    throw new DfsException("下载剧幕文件[" + file.getPath() + "]失败",e);
                }finally {
                    if(encodedFile.exists()){
                        if(!encodedFile.delete()){
                            logger.error("删除临时加密剧幕文件["+encodedFile.getAbsolutePath()+"]失败");
                        }
                    }
                    if(filePath.exists()){
                        if(!filePath.delete()){
                            logger.error("删除临时剧幕文件["+filePath.getAbsolutePath()+"]失败");
                        }
                    }
                }
            }
        }
    }

    /**
     * 开一个进程执行命令
     * @param cmd 街执行的命令
     * @return 命令执行结果
     */
    private boolean runExecute(String cmd){
        Runtime rt = Runtime.getRuntime();
        Process p = null;
        try {
            p = rt.exec(cmd);
            try(BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
            BufferedReader errReader = new BufferedReader(new InputStreamReader(p.getErrorStream()))){
                StringBuilder message = new StringBuilder();
                String line;
                while ((line = errReader.readLine())!=null){
                    message.append(line);
                }
                if(message.length()>0){
                    logger.info("执行命令{}的错误输出：{}",cmd, message);
                }
                message.setLength(0);
                while ((line = br.readLine()) != null){
                    message.append(line);
                }
                if(message.length() > 0){
                    logger.info("执行命令{}的输出消息：{}",cmd, message);
                }
                if(p.waitFor() != 0){
                    if(p.exitValue() != 0){
                        logger.error("命令{}执行失败，退出码：{}*{}",cmd,p.waitFor(),p.exitValue());
                        return false;
                    }
                }
            }
            return true;
        } catch (IOException | InterruptedException e) {
            throw new DfsException("加密视频失败",e);
        }finally {
            if(p != null){
                p.destroy();
            }
        }
    }

    private void validateParam(@NotNull PublishParam param) {
        ErrorCode.PROPERTY_CHECK_ERROR.assertTrue(matchVersion(param.getVersion()),"版本号","剧本版本号格式为x.y或者x.y.z,其中x,y,z为自然数");
    }

    /**
     * 验证版本合法性。x.y或者x.y.z，其中x,y,z都是数字且不以0开始但y和z可以是0，x不能为0
     * @param str 版本字符串
     * @return 合法返回true,否则返回false
     */
    private boolean matchVersion(String str){
        if(str == null || str.trim().length() == 0){
            return false;
        }
        String[] arr = str.split("\\.");
        if(arr.length <= 1 || arr.length >3){
            return false;
        }
        for(String s : arr){
            if("0".equals(s)){
                continue;
            }
            if(s.startsWith("0")){
                return false;
            }
            if(!s.matches("[0-9]*")){
                return false;
            }
        }
        return true;
    }

    private void publishCheck(@NotNull Movie movie) {
        checkParam(movie);
        checkCover(movie);
        checkMovieAssist(movie);
        List<MovieScene> scenes = checkScenes(movie);
        checkClueCards(movie,scenes);
        List<PlayRole> roles = checkPlayRole(movie);
        checkPlayRoleBook(movie,roles);
    }

    private void checkPlayRoleBook(@NotNull Movie movie, @NotNull @NotNullElement List<PlayRole> roles) {
        List<BookChapter> chapters = bookChapterService.movieChapters(movie.getId());
        Map<Integer,BookChapter> seqMap = new HashMap<>();
        chapters.forEach(chapter->{
            ErrorCode.PROPERTY_CHECK_ERROR.assertNotBlank(chapter.getName(),"名称","章节名称不能为空");
            ErrorCode.PROPERTY_CHECK_ERROR.assertFalse(seqMap.containsKey(chapter.getSequence()),
                    ()->new Object[]{"序号",String.format("章节[%s]的序号与[%s]的序号相同,请调整顺序",chapter.getName(),seqMap.get(chapter.getSequence()).getName())});
            seqMap.put(chapter.getSequence(),chapter);
        });
        List<PlayRoleBook> books = playRoleBookService.movieRoleBooks(movie.getId());

        List<PlayRoleBookFile> files = playRoleBookFileService.listInProperty("id",books.stream().map(PlayRoleBook::getFileId).collect(Collectors.toSet()));
        for(BookChapter chapter : chapters){
            for(PlayRole role : roles){
                PlayRoleBook playRoleBook = books.stream().filter(book->Objects.equals(book.getChapterId(),chapter.getId())
                        && Objects.equals(book.getPlayRoleId(),role.getId())).findFirst().orElse(null);
                ErrorCode.PROPERTY_CHECK_ERROR.assertNotNull(playRoleBook,()->new Object[]{"电子书",String.format("[%s]的[%s]电子书未配置",role.getName(),chapter.getName())});
                ErrorCode.PROPERTY_CHECK_ERROR.assertTrue(playRoleBook.getFileId() != null && playRoleBook.getFileId() != 0,
                        ()->new Object[]{"电子书文件",String.format("[%s]的[%s]电子书文件未配置",role.getName(),chapter.getName())});
                PlayRoleBookFile file = IdOperators.find(files,playRoleBook.getFileId());
                ErrorCode.PROPERTY_CHECK_ERROR.assertNotNull(file,()->new Object[]{"电子书文件",String.format("[%s]的[%s]电子书文件未找到",role.getName(),chapter.getName())});
                ErrorCode.PROPERTY_CHECK_ERROR.assertTrue(webDfsService.exist(webDfsService.dfsFile(file.getPath())),
                        ()->new Object[]{"电子书文件",String.format("[%s]的[%s]电子书文件不存在",role.getName(),chapter.getName())});
                ErrorCode.PROPERTY_CHECK_ERROR.assertNotBlank(playRoleBook.getFilePath(),()->new Object[]{"电子书文件",String.format("[%s]的[%s]电子书文件存储路径不能为空",role.getName(),chapter.getName())});
            }
        }
    }

    private List<PlayRole> checkPlayRole(@NotNull Movie movie) {
        List<PlayRole> list = playRoleService.list(movie.getId());
        for(PlayRole playRole : list){
            ErrorCode.PROPERTY_CHECK_ERROR.assertNotBlank(playRole.getName(),"名称","角色名称不能为空");
            ErrorCode.PROPERTY_CHECK_ERROR.assertTrue(playRole.getFileId() != null && playRole.getFileId() != 0,
                    ()->new Object[]{"角色文件",String.format("角色[%s]未配置文件",playRole.getName())});
            PlayRoleFile file = playRoleFileService.select(playRole.getFileId());
            ErrorCode.PLAY_ROLE_FILE_NOT_EXIST.assertNotNull(file,playRole.getName());
            ErrorCode.PLAY_ROLE_FILE_NOT_EXIST.assertTrue(webDfsService.exist(webDfsService.dfsFile(file.getPath())),playRole.getName());
            ErrorCode.PROPERTY_CHECK_ERROR.assertNotBlank(playRole.getFilePath(),"角色文件","角色文件存储路径不能为空");
        }
        return list;
    }

    private void checkClueCards(@NotNull Movie movie,@NotNull @NotNullElement List<MovieScene> scenes) {
        List<ClueCardGroup> list = clueCardGroupService.clueCardGroups(movie.getId());
        List<Integer> seqList = scenes.stream().map(MovieScene::getSequence).collect(Collectors.toList());
        for(ClueCardGroup group : list){
            ErrorCode.PROPERTY_CHECK_ERROR.assertNotBlank(group.getName(),"名称","线索分组的名称不能为空");
            ErrorCode.PROPERTY_CHECK_ERROR.assertNotBlank(group.getScenes(),()->new Object[]{"所属剧幕",String.format("线索分组[%s]的所属剧幕为空", group.getName())});
            List<Integer> theScenes = NumberUtils.toInts(group.getScenes(),ClueCardGroup.SCENE_SEQUENCE_SPLIT);
            ErrorCode.PROPERTY_CHECK_ERROR.assertTrue(seqList.containsAll(theScenes),()->new Object[]{"所属剧幕",String.format("线索分组[%s]所属剧幕不存在,请重新选择所属剧幕",group.getName())});
        }
        List<ClueCard> clueCards = clueCardService.clueCards(list.stream().map(ClueCardGroup::getId).collect(Collectors.toSet()));
        Long[] groupIds = list.stream().map(ClueCardGroup::getId).distinct().toArray(Long[]::new);
        for(ClueCard clueCard : clueCards){
            ErrorCode.PROPERTY_CHECK_ERROR.assertNotBlank(clueCard.getName(),"名称","线索名称不能为空");
            ErrorCode.PROPERTY_CHECK_ERROR.assertAnyOf(clueCard.getType(),ClueCard.CLUE_CARD_TYPES,()->new Object[]{"类型",String.format("线索[%s]的类型[%s]错误",clueCard.getName(),clueCard.getType())});
            ErrorCode.PROPERTY_CHECK_ERROR.assertAnyOf(clueCard.getGroupId(),groupIds,()->new Object[]{"所属分组",String.format("线索[%s]的分组配置错误",clueCard.getName())});
            if(clueCard.getHdFileId() != null && clueCard.getHdFileId() != 0){
                ClueCardFile file = clueCardFileService.select(clueCard.getHdFileId());
                ErrorCode.PROPERTY_CHECK_ERROR.assertNotNull(file,()->new Object[]{"高清文件",String.format("线索卡[%s]的高清文件不存在",clueCard.getName())});
                ErrorCode.PROPERTY_CHECK_ERROR.assertTrue(webDfsService.exist(webDfsService.dfsFile(file.getPath())),()->new Object[]{"高清文件",String.format("线索卡[%s]的高清文件不存在",clueCard.getName())});
                ErrorCode.PROPERTY_CHECK_ERROR.assertNotBlank(clueCard.getHdFile(),()->new Object[]{"高清文件",String.format("线索卡[%s]的高清文件存储路径不能为空",clueCard.getName())});
            }
            if(clueCard.getSdFileId() != null && clueCard.getSdFileId() != 0){
                ClueCardFile file = clueCardFileService.select(clueCard.getSdFileId());
                ErrorCode.PROPERTY_CHECK_ERROR.assertNotNull(file,()->new Object[]{"标清文件",String.format("线索卡[%s]的标清文件不存在",clueCard.getName())});
                ErrorCode.PROPERTY_CHECK_ERROR.assertTrue(webDfsService.exist(webDfsService.dfsFile(file.getPath())),()->new Object[]{"标清文件",String.format("线索卡[%s]的标清文件不存在",clueCard.getName())});
                ErrorCode.PROPERTY_CHECK_ERROR.assertNotBlank(clueCard.getSdFile(),()->new Object[]{"标清文件",String.format("线索卡[%s]的标清文件存储路径不能为空",clueCard.getName())});
            }
            ErrorCode.PROPERTY_CHECK_ERROR.assertTrue(clueCard.getHdFileId() != null && clueCard.getHdFileId() != 0,
                    ()->new Object[]{"线索文件",String.format("线索[%s]的高清文件不能为空",clueCard.getName())});
            ErrorCode.PROPERTY_CHECK_ERROR.assertTrue(clueCard.getSdFileId() != null && clueCard.getSdFileId() != 0,
                    ()->new Object[]{"线索文件",String.format("线索[%s]的标清文件不能为空",clueCard.getName())});
        }
    }

    @NotNull
    @NotNullElement
    private List<MovieScene> checkScenes(@NotNull Movie movie) {
        List<MovieScene> scenes = movieSceneService.movieScenes(movie.getId());
        ErrorCode.PROPERTY_CHECK_ERROR.assertNotEmpty(scenes,"剧幕列表","剧幕列表不能为空");
        Map<Integer,MovieScene> seqMap = new HashMap<>();
        for (MovieScene scene : scenes){
            ErrorCode.PROPERTY_CHECK_ERROR.assertNotBlank(scene.getName(),"名称","剧幕名称不能为空");
            ErrorCode.SCENE_FILE_NOT_EXIST.assertNotBlank(scene.getFile(),scene.getName());
            MovieSceneFile file = movieSceneFileService.select(scene.getFileId());
            ErrorCode.SCENE_FILE_NOT_EXIST.assertNotNull(file,scene.getName());
            ErrorCode.SCENE_FILE_NOT_EXIST.assertTrue(webDfsService.exist(webDfsService.dfsFile(file.getPath())),scene.getName());
            ErrorCode.PROPERTY_CHECK_ERROR.assertAnyOf(scene.getPlayMode(),MovieScene.playModes,"播放模式","请选择正确值");
            if(seqMap.containsKey(scene.getSequence())){
                MovieScene theScene = seqMap.get(scene.getSequence());
                ErrorCode.PROPERTY_CHECK_ERROR.fail("剧幕序号",String.format("剧幕[%s]的序号与[%s]的序号相同,请调整剧幕顺序",scene.getName(),theScene.getName()));
            }else{
                seqMap.put(scene.getSequence(),scene);
            }
        }
        return scenes;
    }


    private void checkParam(@NotNull Movie movie){
        ErrorCode.NAME_BLANK_ERROR.assertNotBlank(movie.getName());
    }
    private void checkCover(@NotNull Movie movie){
        ErrorCode.COVER_NOT_EXIST.assertNotBlank(movie.getCover(),movie.getId());
        MovieCoverFile file = movieCoverFileService.select(movie.getCoverFileId());
        ErrorCode.COVER_NOT_EXIST.assertNotNull(file,movie.getName());
        ErrorCode.COVER_FILE_NOT_FOUND.assertTrue(webDfsService.exist(webDfsService.dfsFile(file.getPath())),movie.getName(),file.getPath());
    }

    private void checkMovieAssist(@NotNull Movie movie){
        if(!DatabaseUtils.isPoIdValid(movie.getAssistFileId())){//剧本手册是空的,那可能就是没有配置,这是可以的
            return;
        }
        MovieAssistFile file = movieAssistFileService.select(movie.getAssistFileId());
        ErrorCode.MOVIE_ASSIST_FILE_NOT_FOUND.assertNotNull(file,movie.getName(),movie.getAssist());
        ErrorCode.MOVIE_ASSIST_FILE_NOT_FOUND.assertTrue(webDfsService.exist(webDfsService.dfsFile(file.getPath())),movie.getName(),file.getPath());
    }
}
